/**
 * Copyright 2019 吉鼎科技.

 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.easyplatform.web.ext.cmez;

import cn.easyplatform.lang.Strings;
import cn.easyplatform.web.ext.Assignable;
import cn.easyplatform.web.ext.Writable;
import cn.easyplatform.web.ext.ZkExt;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.zkoss.lang.Objects;
import org.zkoss.util.resource.Labels;
import org.zkoss.web.servlet.http.Encodes;
import org.zkoss.zk.au.out.AuFocus;
import org.zkoss.zk.ui.WebApp;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.InputEvent;
import org.zkoss.zk.ui.ext.Disable;
import org.zkoss.zk.ui.ext.Readonly;
import org.zkoss.zul.impl.XulElement;

import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author create by <a href="mailto:davidchen@epclouds.com">littleDog</a> on <br/>
 * @since 2.0.0 <br/>
 */
public class CMeditor extends XulElement implements Disable, Readonly,
        Assignable, Writable, ZkExt {

    public final static String ON_CURSOR_ACTIVITY = "onCursorActivity";

    static {
        addClientEvent(CMeditor.class, Events.ON_CHANGE, CE_IMPORTANT
                | CE_REPEAT_IGNORE);
        addClientEvent(CMeditor.class, Events.ON_CHANGING, CE_IMPORTANT | CE_REPEAT_IGNORE);
        addClientEvent(CMeditor.class, Events.ON_FOCUS, CE_IMPORTANT
                | CE_REPEAT_IGNORE);
        addClientEvent(CMeditor.class, Events.ON_BLUR, CE_IMPORTANT
                | CE_REPEAT_IGNORE);
        addClientEvent(CMeditor.class, GutterEvent.ON_ADD_MARKER,
                CE_REPEAT_IGNORE);
        addClientEvent(CMeditor.class, GutterEvent.ON_REMOVE_MARKER,
                CE_REPEAT_IGNORE);
        addClientEvent(CMeditor.class, GutterEvent.ON_CLEAR_MARKER,
                CE_REPEAT_IGNORE);
        addClientEvent(CMeditor.class, ON_CURSOR_ACTIVITY, CE_REPEAT_IGNORE);
    }

    /**
     *
     */
    private static final long serialVersionUID = 1L;

    /**
     * 编辑内容类型：xml|javascript|eppage|epscript|css|html|properties|json
     * text/x-sql|text
     * /x-mysql|text/x-mariadb|text/x-cassandra|text/x-plsql|text/
     * x-mssql|text/x-hive|text/x-pgsql|text/x-gql
     */
    private String _mode = "xml";

    /**
     * 文本内容
     */
    private Object _value = "";

    /**
     *
     */
    private boolean _lineNumbers = true;

    private boolean _styleActiveLine;

    private boolean _autoCloseTags;

    private boolean _foldGutter;

    private String _theme = "default";

    private boolean _debug;

    private boolean _disabled;

    private String _scrollbarStyle = "native";

    private boolean _lineWrapping = true;

    private boolean _autocomplete = true;

    private boolean _highlightSelection = false;

    private boolean _matchTags;

    private boolean _showTrailingSpace;

    private int _tabSize = 4;

    private int _indentUnit = 2;

    private boolean _indentWithTabs;

    private boolean _fullScreen;

    private boolean _smartIndent;

    private String _border = "normal";

    // type :normal|diff
    private String _type = "normal";
    // 要比较的对象
    private String _orig;
    // 要比较的对象是否排成一线
    private String _connect;
    // 是否折叠
    private boolean _collapse;
    // 第3个要比较的内容
    private String _origLeft;
    // 是否获取焦点
    private boolean _focus;
    //是否要显示关键字
    private Object _hints;
    //显示语法错误
    private boolean _lint;

    private String _filePath;

    public void setFilePath(String filePath) {
        this._filePath = filePath;
    }

    public boolean isLint() {
        return _lint;
    }

    public void setLint(boolean lint) {
        if (_lint != lint) {
            this._lint = lint;
            smartUpdate("lint", lint);
        }
    }

    public Object getHints() {
        return _hints;
    }

    public void setHints(Object hints) {
        if (!Objects.equals(_hints, hints)) {
            this._hints = hints;
            if (hints instanceof String)
                smartUpdate("hints", _hints);
            else if (hints instanceof Object[]) {
                Object[] data = (Object[]) hints;
                if (data.length > 0) {
                    StringBuilder sb = new StringBuilder();
                    for (Object o : data)
                        sb.append(o).append(";");
                    sb.deleteCharAt(sb.length() - 1);
                    smartUpdate("hints", sb.toString());
                }
            } else if (hints instanceof List<?>) {
                List<?> data = (List<?>) hints;
                if (data.size() > 0) {
                    StringBuilder sb = new StringBuilder();
                    for (Object o : data)
                        sb.append(o).append(";");
                    sb.deleteCharAt(sb.length() - 1);
                    smartUpdate("hints", sb.toString());
                }
            }
        }
    }

    /**
     * @return the focus
     */
    public boolean isFocus() {
        return _focus;
    }

    /**
     * @param focus the focus to set
     */
    public void setFocus(boolean focus) {
        if (_focus != focus) {
            this._focus = focus;
            smartUpdate("focus", focus);
        }
    }

    /**
     * @param line
     * @param ch
     */
    public void setCursor(int line, int ch) {
        Map<String, Integer> cursor = new HashMap<String, Integer>();
        cursor.put("line", line);
        cursor.put("ch", ch);
        smartUpdate("cursor", cursor);
    }

    /**
     * @param text
     */
    public void insertText(String text) {
        insertText(-1, -1, text);
    }

    /**
     * @param line
     * @param ch
     * @param text
     */
    public void insertText(int line, int ch, String text) {
        if (Strings.isBlank(text))
            return;
        Map<String, Object> data = new HashMap<String, Object>();
        if (line >= 0 && ch >= 0) {
            data.put("line", line);
            data.put("ch", ch);
        }
        data.put("text", text);
        smartUpdate("insertText", data);
    }

    /**
     * @return the connect
     */
    public String getConnect() {
        return _connect;
    }

    /**
     * @param connect the connect to set
     */
    public void setConnect(String connect) {
        if (!Objects.equals(_connect, connect)) {
            this._connect = connect;
            smartUpdate("connect", connect);
        }
    }

    /**
     * @return the type
     */
    public String getType() {
        return _type;
    }

    /**
     * @param type the type to set
     */
    public void setType(String type) {
        if (!Objects.equals(_type, type)) {
            this._type = type;
            smartUpdate("type", type);
        }
    }

    /**
     * @return the orig
     */
    public String getOrig() {
        return _orig;
    }

    /**
     * @param orig the orig to set
     */
    public void setOrig(String orig) {
        if (!Objects.equals(_orig, orig)) {
            this._orig = orig;
            smartUpdate("orig", orig);
        }
    }

    /**
     * @return the origLeft
     */
    public String getOrigLeft() {
        return _origLeft;
    }

    /**
     * @param origLeft the origLeft to set
     */
    public void setOrigLeft(String origLeft) {
        if (!Objects.equals(_origLeft, origLeft)) {
            this._origLeft = origLeft;
            smartUpdate("origLeft", origLeft);
        }
    }

    /**
     * @return the collapse
     */
    public boolean isCollapse() {
        return _collapse;
    }

    /**
     * @param collapse the collapse to set
     */
    public void setCollapse(boolean collapse) {
        if (this._collapse != collapse) {
            this._collapse = collapse;
            smartUpdate("collapse", collapse);
        }
    }

    /**
     * @return the smartIndent
     */
    public boolean isSmartIndent() {
        return _smartIndent;
    }

    /**
     * @param smartIndent the smartIndent to set
     */
    public void setSmartIndent(boolean smartIndent) {
        if (this._smartIndent != smartIndent) {
            this._smartIndent = smartIndent;
            smartUpdate("smartIndent", _smartIndent);
        }
    }

    /**
     * @return the fullScreen
     */
    public boolean isFullScreen() {
        return _fullScreen;
    }

    /**
     * @param fullScreen the fullScreen to set
     */
    public void setFullScreen(boolean fullScreen) {
        if (this._fullScreen != fullScreen) {
            this._fullScreen = fullScreen;
            smartUpdate("fullScreen", _fullScreen);
        }
    }

    /**
     * @return the tabSize
     */
    public int getTabSize() {
        return _tabSize;
    }

    /**
     * @param tabSize the tabSize to set
     */
    public void setTabSize(int tabSize) {
        if (this._tabSize != tabSize) {
            this._tabSize = tabSize;
            smartUpdate("tabSize", _tabSize);
        }
    }

    /**
     * @return the indentUnit
     */
    public int getIndentUnit() {
        return _indentUnit;
    }

    /**
     * @param indentUnit the indentUnit to set
     */
    public void setIndentUnit(int indentUnit) {
        if (this._indentUnit != indentUnit) {
            this._indentUnit = indentUnit;
            smartUpdate("indentUnit", _indentUnit);
        }
    }

    /**
     * @return the indentWithTabs
     */
    public boolean isIndentWithTabs() {
        return _indentWithTabs;
    }

    /**
     * @param indentWithTabs the indentWithTabs to set
     */
    public void setIndentWithTabs(boolean indentWithTabs) {
        if (this._indentWithTabs != indentWithTabs) {
            this._indentWithTabs = indentWithTabs;
            smartUpdate("indentWithTabs", _indentWithTabs);
        }
    }

    /**
     * @return the showTrailingSpace
     */
    public boolean isShowTrailingSpace() {
        return _showTrailingSpace;
    }

    /**
     * @param showTrailingSpace the showTrailingSpace to set
     */
    public void setShowTrailingSpace(boolean showTrailingSpace) {
        if (this._showTrailingSpace != showTrailingSpace) {
            this._showTrailingSpace = showTrailingSpace;
            smartUpdate("showTrailingSpace", _showTrailingSpace);
        }
    }

    /**
     * @return the matchTags
     */
    public boolean isMatchTags() {
        return _matchTags;
    }

    /**
     * @param matchTags the matchTags to set
     */
    public void setMatchTags(boolean matchTags) {
        if (_matchTags != matchTags) {
            this._matchTags = matchTags;
            smartUpdate("matchTags", _matchTags);
        }
    }

    /**
     * @return the highlightSelection
     */
    public boolean isHighlightSelection() {
        return _highlightSelection;
    }

    /**
     * @param highlightSelection the highlightSelection to set
     */
    public void setHighlightSelection(boolean highlightSelection) {
        if (_highlightSelection != highlightSelection) {
            this._highlightSelection = highlightSelection;
            smartUpdate("highlightSelection", _highlightSelection);
        }
    }

    /**
     * @return the autocomplete
     */
    public boolean isAutocomplete() {
        return _autocomplete;
    }

    /**
     * @param autocomplete the autocomplete to set
     */
    public void setAutocomplete(boolean autocomplete) {
        if (autocomplete != this._autocomplete) {
            this._autocomplete = autocomplete;
            smartUpdate("autocomplete", _autocomplete);
        }
    }

    /**
     * @return the mode
     */
    public String getMode() {
        return _mode;
    }

    /**
     * @param mode the mode to set
     */
    public void setMode(String mode) {
        if (!Objects.equals(_mode, mode)) {
            this._mode = mode;
            smartUpdate("mode", mode);
        }
    }

    /**
     * @return the scrollbarStyle
     */
    public String getScrollbarStyle() {
        return _scrollbarStyle;
    }

    /**
     * @param scrollbarStyle the scrollbarStyle to set
     */
    public void setScrollbarStyle(String scrollbarStyle) {
        if (!Objects.equals(_scrollbarStyle, scrollbarStyle)) {
            this._scrollbarStyle = scrollbarStyle;
            smartUpdate("scrollbarStyle", scrollbarStyle);
        }
    }

    /**
     * @return the lineWrapping
     */
    public boolean isLineWrapping() {
        return _lineWrapping;
    }

    /**
     * @param lineWrapping the lineWrapping to set
     */
    public void setLineWrapping(boolean lineWrapping) {
        if (lineWrapping != this._lineWrapping) {
            this._lineWrapping = lineWrapping;
            smartUpdate("lineWrapping", lineWrapping);
        }
    }

    @Override
    public boolean isDisabled() {
        return _disabled;
    }

    @Override
    public void setDisabled(boolean disabled) {
        if (_disabled != disabled) {
            _disabled = disabled;
            smartUpdate("disabled", disabled);
        }
    }

    @Override
    public boolean isReadonly() {
        return isDisabled();
    }

    @Override
    public void setReadonly(boolean readonly) {
        setDisabled(readonly);
    }

    /**
     * @return the debug
     */
    public boolean isDebug() {
        return _debug;
    }

    /**
     * @param debug the debug to set
     */
    public void setDebug(boolean debug) {
        if (_debug != debug) {
            this._debug = debug;
            smartUpdate("debug", debug);
        }
    }

    /**
     * @return the theme
     */
    public String getTheme() {
        return _theme;
    }

    /**
     * @param theme the theme to set
     */
    public void setTheme(String theme) {
        if (Strings.isBlank(theme))
            theme = "default";
        if (!Objects.equals(_theme, theme)) {
            this._theme = theme;
            smartUpdate("theme", theme);
        }
    }

    /**
     * @return the showLineNumbers
     */
    public boolean isLineNumbers() {
        return _lineNumbers;
    }

    /**
     * @param lineNumbers the showLineNumbers to set
     */
    public void setLineNumbers(boolean lineNumbers) {
        if (_lineNumbers != lineNumbers) {
            this._lineNumbers = lineNumbers;
            smartUpdate("lineNumbers", lineNumbers);
        }
    }

    /**
     * @return the styleActiveLine
     */
    public boolean isStyleActiveLine() {
        return _styleActiveLine;
    }

    /**
     * @param styleActiveLine the styleActiveLine to set
     */
    public void setStyleActiveLine(boolean styleActiveLine) {
        if (_styleActiveLine != styleActiveLine) {
            this._styleActiveLine = styleActiveLine;
            smartUpdate("styleActiveLine", styleActiveLine);
        }
    }

    /**
     * @return the autoCloseTags
     */
    public boolean isAutoCloseTags() {
        return _autoCloseTags;
    }

    /**
     * @param autoCloseTags the autoCloseTags to set
     */
    public void setAutoCloseTags(boolean autoCloseTags) {
        if (_autoCloseTags != autoCloseTags) {
            this._autoCloseTags = autoCloseTags;
            smartUpdate("autoCloseTags", autoCloseTags);
        }
    }

    /**
     * @return the foldGutter
     */
    public boolean isFoldGutter() {
        return _foldGutter;
    }

    /**
     * @param foldGutter the foldGutter to set
     */
    public void setFoldGutter(boolean foldGutter) {
        if (_foldGutter != foldGutter) {
            this._foldGutter = foldGutter;
            smartUpdate("foldGutter", foldGutter);
        }
    }

    /**
     * @return the value
     */
    public Object getValue() {
        return _value;
    }

    /**
     * @param value the value to set
     */
    public void setValue(Object value) {
        if (Strings.isBlank((String) value))
            value = "";
        if (!Objects.equals(_value, value)) {
            this._value = value;
            smartUpdate("value", _value);
        }
    }

    public String getBorder() {
        return _border;
    }

    public void setBorder(String border) {
        if (!_border.equals(border)) {
            this._border = border;
            smartUpdate("border", border);
        }
    }

    public void focus() {
        response(new AuFocus(this));
    }

    public void exeCmd(String cmd) {
        smartUpdate("exeCmd", cmd);
    }

    public void formatAll() {
        smartUpdate("exeCmd", "formatAll");
    }

    protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer)
            throws java.io.IOException {
        super.renderProperties(renderer);
        renderer.render("value", _value == null ? "" : _value.toString().trim());
        renderer.render("mode", _mode);
        renderer.render("lineNumbers", _lineNumbers);
        renderer.render("styleActiveLine", _styleActiveLine);
        renderer.render("autoCloseTags", _autoCloseTags);
        renderer.render("foldGutter", _foldGutter);
        renderer.render("theme", _theme);
        renderer.render("debug", _debug);
        renderer.render("disabled", _disabled);
        renderer.render("lineWrapping", _lineWrapping);
        renderer.render("scrollbarStyle", _scrollbarStyle);
        renderer.render("autocomplete", _autocomplete);
        renderer.render("highlightSelection", _highlightSelection);
        renderer.render("matchTags", _matchTags);
        renderer.render("showTrailingSpace", _showTrailingSpace);
        renderer.render("indentUnit", _indentUnit);
        renderer.render("tabSize", _tabSize);
        renderer.render("indentWithTabs", _indentWithTabs);
        renderer.render("fullScreen", _fullScreen);
        renderer.render("smartIndent", _smartIndent);
        renderer.render("border", _border);
        renderer.render("type", _type);
        renderer.render("collapse", _collapse);
        renderer.render("orig", _orig);
        renderer.render("origLeft", _origLeft);
        renderer.render("connect", _connect);
        renderer.render("focus", _focus);
        renderer.render("lint", _lint);

        if (_hints != null) {
            if (_hints instanceof String)
                renderer.render("hints", _hints);
            else if (_hints instanceof Object[]) {
                Object[] data = (Object[]) _hints;
                if (data.length > 0) {
                    StringBuilder sb = new StringBuilder();
                    for (Object o : data)
                        sb.append(o).append(";");
                    sb.deleteCharAt(sb.length() - 1);
                    renderer.render("hints", sb.toString());
                }
            } else if (_hints instanceof List<?>) {
                List<?> data = (List<?>) _hints;
                if (data.size() > 0) {
                    StringBuilder sb = new StringBuilder();
                    for (Object o : data)
                        sb.append(o).append(";");
                    sb.deleteCharAt(sb.length() - 1);
                    renderer.render("hints", sb.toString());
                }
            }
        }
    }

    public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
        final String cmd = request.getCommand();
        if (cmd.equals(Events.ON_CHANGE) || cmd.equals(Events.ON_CHANGING)) {
            String value = (String) request.getData().get("value");
            InputEvent evt = new InputEvent(cmd, this, value, _value);
            this._value = value;
            Events.postEvent(evt);
        } else if (cmd.endsWith("Marker")) {
            Object text = request.getData().get("text");
            int lineNo = (Integer) request.getData().get("line");
            GutterEvent evt = new GutterEvent(cmd, this, text, lineNo);
            Events.postEvent(evt);
        } else if (cmd.equals(ON_CURSOR_ACTIVITY)) {
            Event evt = new Event(ON_CURSOR_ACTIVITY, this, request.getData());
            Events.postEvent(evt);
        } else if (cmd.equals(Events.ON_FOCUS)) {
            this._focus = true;
            Events.postEvent(Events.ON_FOCUS, this, null);
        } else if (cmd.equals(Events.ON_BLUR)) {
            this._focus = false;
            Events.postEvent(Events.ON_BLUR, this, null);
        } else if (cmd.equals(Events.ON_DROP)) {
            org.zkoss.zk.ui.event.DropEvent de = org.zkoss.zk.ui.event.DropEvent
                    .getDropEvent(request);
            DropEvent evt = new DropEvent(de.getName(), de.getTarget(),
                    de.getDragged(), de.getX(), de.getY(), de.getPageX(),
                    de.getPageY(), de.getKeys(), (Integer) request.getData()
                    .get("line"), (Integer) request.getData().get("ch"));
            Events.postEvent(evt);
        } else
            super.service(request, everError);
    }

    @Override
    public String getFilePath() {
        return _filePath;
    }

    @Override
    public String write(String fileName, byte[] data) {
        if (cn.easyplatform.lang.Strings.isBlank(_filePath)) {
            throw new RuntimeException(Labels.getLabel("component.property.not.found", new Object[]{"<cmeditor>", "filePath"}));
        } else {
            if (isObfuscation())
                fileName = RandomStringUtils.randomAlphanumeric(12) + "." + FilenameUtils.getExtension(fileName);
            Writable proxy = (Writable) getAttribute("$proxy");
            WebApp webApp = this.getDesktop().getWebApp();
            if (proxy != null)
                return webApp.getServletContext() + proxy.write(fileName, data);
            else {
                if (_filePath.startsWith("$7"))
                    throw new RuntimeException("Permission denied");
                try {
                    String file = FilenameUtils.normalize(webApp.getRealPath(_filePath)
                            + "/" + fileName);
                    FileUtils.writeByteArrayToFile(new File(file),
                            data);
                    return webApp.getServletContext().getContextPath() + _filePath + "/" + Encodes.encodeURI(fileName);
                } catch (Exception e) {
                    throw new RuntimeException(e.getMessage());
                }
            }
        }
    }
}
